/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.droidlogic.inputmethod.remote;

import com.droidlogic.inputmethod.remote.RemoteIME.DecodingInfo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.TranslateAnimation;
import android.view.animation.Animation.AnimationListener;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.ViewFlipper;

interface ArrowUpdater {
    void updateArrowStatus();
}


/**
 * Container used to host the two candidate views. When user drags on candidate
 * view, animation is used to dismiss the current candidate view and show a new
 * one. These two candidate views and their parent are hosted by this container.
 * <p>
 * Besides the candidate views, there are two arrow views to show the page
 * forward/backward arrows.
 * </p>
 */
public class CandidatesContainer extends RelativeLayout implements
        OnTouchListener, AnimationListener, ArrowUpdater {
        /**
         * Alpha value to show an enabled arrow.
         */
        private static int ARROW_ALPHA_ENABLED = 0xff;

        /**
         * Alpha value to show an disabled arrow.
         */
        private static int ARROW_ALPHA_DISABLED = 0x40;

        /**
         * Animation time to show a new candidate view and dismiss the old one.
         */
        private static int ANIMATION_TIME = 200;

        /**
         * Listener used to notify IME that user clicks a candidate, or navigate
         * between them.
         */
        private CandidateViewListener mCvListener;

        /**
         * The left arrow button used to show previous page.
         */
        private ImageButton mLeftArrowBtn;

        /**
         * The right arrow button used to show next page.
         */
        private ImageButton mRightArrowBtn;

        /**
         * Decoding result to show.
         */
        private DecodingInfo mDecInfo;

        /**
         * The animation view used to show candidates. It contains two views.
         * Normally, the candidates are shown one of them. When user navigates to
         * another page, animation effect will be performed.
         */
        private ViewFlipper mFlipper;

        /**
         * The x offset of the flipper in this container.
         */
        private int xOffsetForFlipper;

        /**
         * Animation used by the incoming view when the user navigates to a left
         * page.
         */
        private Animation mInAnimPushLeft;

        /**
         * Animation used by the incoming view when the user navigates to a right
         * page.
         */
        private Animation mInAnimPushRight;

        /**
         * Animation used by the incoming view when the user navigates to a page
         * above. If the page navigation is triggered by DOWN key, this animation is
         * used.
         */
        private Animation mInAnimPushUp;

        /**
         * Animation used by the incoming view when the user navigates to a page
         * below. If the page navigation is triggered by UP key, this animation is
         * used.
         */
        private Animation mInAnimPushDown;

        /**
         * Animation used by the outgoing view when the user navigates to a left
         * page.
         */
        private Animation mOutAnimPushLeft;

        /**
         * Animation used by the outgoing view when the user navigates to a right
         * page.
         */
        private Animation mOutAnimPushRight;

        /**
         * Animation used by the outgoing view when the user navigates to a page
         * above. If the page navigation is triggered by DOWN key, this animation is
         * used.
         */
        private Animation mOutAnimPushUp;

        /**
         * Animation used by the incoming view when the user navigates to a page
         * below. If the page navigation is triggered by UP key, this animation is
         * used.
         */
        private Animation mOutAnimPushDown;

        /**
         * Animation object which is used for the incoming view currently.
         */
        private Animation mInAnimInUse;

        /**
         * Animation object which is used for the outgoing view currently.
         */
        private Animation mOutAnimInUse;

        /**
         * Current page number in display.
         */
        private int mCurrentPage = -1;

        public CandidatesContainer ( Context context, AttributeSet attrs ) {
            super ( context, attrs );
        }

        public void initialize ( CandidateViewListener cvListener,
                                 BalloonHint balloonHint, GestureDetector gestureDetector ) {
            mCvListener = cvListener;
            mLeftArrowBtn = ( ImageButton ) findViewById ( R.id.arrow_left_btn );
            mRightArrowBtn = ( ImageButton ) findViewById ( R.id.arrow_right_btn );
            mLeftArrowBtn.setOnTouchListener ( this );
            mRightArrowBtn.setOnTouchListener ( this );
            mFlipper = ( ViewFlipper ) findViewById ( R.id.candidate_flipper );
            mFlipper.setMeasureAllChildren ( true );
            invalidate();
            requestLayout();
            for ( int i = 0; i < mFlipper.getChildCount(); i++ ) {
                CandidateView cv = ( CandidateView ) mFlipper.getChildAt ( i );
                cv.initialize ( this, balloonHint, gestureDetector, mCvListener );
            }
        }

        public void showCandidates ( RemoteIME.DecodingInfo decInfo,
                                     boolean enableActiveHighlight ) {
            if ( null == decInfo ) { return; }
            mDecInfo = decInfo;
            mCurrentPage = 0;
            if ( decInfo.isCandidatesListEmpty() ) {
                showArrow ( mLeftArrowBtn, false );
                showArrow ( mRightArrowBtn, false );
            } else {
                showArrow ( mLeftArrowBtn, true );
                showArrow ( mRightArrowBtn, true );
            }
            for ( int i = 0; i < mFlipper.getChildCount(); i++ ) {
                CandidateView cv = ( CandidateView ) mFlipper.getChildAt ( i );
                cv.setDecodingInfo ( mDecInfo );
            }
            stopAnimation();
            CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
            cv.showPage ( mCurrentPage, 0, enableActiveHighlight );
            updateArrowStatus();
            invalidate();
        }

        public int getCurrentPage() {
            return mCurrentPage;
        }

        public void enableActiveHighlight ( boolean enableActiveHighlight ) {
            CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
            cv.enableActiveHighlight ( enableActiveHighlight );
            invalidate();
        }

        @Override
        protected void onMeasure ( int widthMeasureSpec, int heightMeasureSpec ) {
            Environment env = Environment.getInstance();
            int measuredWidth = env.getScreenWidth();
            int measuredHeight = getPaddingTop();
            measuredHeight += env.getHeightForCandidates();
            widthMeasureSpec = MeasureSpec.makeMeasureSpec ( measuredWidth,
                               MeasureSpec.EXACTLY );
            heightMeasureSpec = MeasureSpec.makeMeasureSpec ( measuredHeight,
                                MeasureSpec.EXACTLY );
            super.onMeasure ( widthMeasureSpec, heightMeasureSpec );
            if ( null != mLeftArrowBtn ) {
                xOffsetForFlipper = mLeftArrowBtn.getMeasuredWidth();
            }
        }

        public boolean activeCurseBackward() {
            if ( mFlipper.isFlipping() || null == mDecInfo ) {
                return false;
            }
            CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
            if ( cv.activeCurseBackward() ) {
                cv.invalidate();
                return true;
            } else {
                return pageBackward ( true, true );
            }
        }

        public boolean activeCurseForward() {
            if ( mFlipper.isFlipping() || null == mDecInfo ) {
                return false;
            }
            CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
            if ( cv.activeCursorForward() ) {
                cv.invalidate();
                return true;
            } else {
                return pageForward ( true, true );
            }
        }

        public boolean pageBackward ( boolean animLeftRight,
                                      boolean enableActiveHighlight ) {
            if ( null == mDecInfo ) { return false; }
            if ( mFlipper.isFlipping() || 0 == mCurrentPage ) { return false; }
            int child = mFlipper.getDisplayedChild();
            int childNext = ( child + 1 ) % 2;
            CandidateView cv = ( CandidateView ) mFlipper.getChildAt ( child );
            CandidateView cvNext = ( CandidateView ) mFlipper.getChildAt ( childNext );
            mCurrentPage--;
            int activeCandInPage = cv.getActiveCandiatePosInPage();
            if ( animLeftRight )
                activeCandInPage = mDecInfo.mPageStart.elementAt ( mCurrentPage + 1 )
                                   - mDecInfo.mPageStart.elementAt ( mCurrentPage ) - 1;
            cvNext.showPage ( mCurrentPage, activeCandInPage, enableActiveHighlight );
            loadAnimation ( animLeftRight, false );
            startAnimation();
            updateArrowStatus();
            return true;
        }

        public boolean pageForward ( boolean animLeftRight,
                                     boolean enableActiveHighlight ) {
            if ( null == mDecInfo ) { return false; }
            if ( mFlipper.isFlipping() || !mDecInfo.preparePage ( mCurrentPage + 1 ) ) {
                return false;
            }
            int child = mFlipper.getDisplayedChild();
            int childNext = ( child + 1 ) % 2;
            CandidateView cv = ( CandidateView ) mFlipper.getChildAt ( child );
            int activeCandInPage = cv.getActiveCandiatePosInPage();
            cv.enableActiveHighlight ( enableActiveHighlight );
            CandidateView cvNext = ( CandidateView ) mFlipper.getChildAt ( childNext );
            mCurrentPage++;
            if ( animLeftRight ) { activeCandInPage = 0; }
            cvNext.showPage ( mCurrentPage, activeCandInPage, enableActiveHighlight );
            loadAnimation ( animLeftRight, true );
            startAnimation();
            updateArrowStatus();
            return true;
        }

        public int getActiveCandiatePos() {
            if ( null == mDecInfo ) { return -1; }
            CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
            return cv.getActiveCandiatePosGlobal();
        }

        public void updateArrowStatus() {
            if ( mCurrentPage < 0 ) { return; }
            boolean forwardEnabled = mDecInfo.pageForwardable ( mCurrentPage );
            boolean backwardEnabled = mDecInfo.pageBackwardable ( mCurrentPage );
            if ( backwardEnabled ) {
                enableArrow ( mLeftArrowBtn, true );
            } else {
                enableArrow ( mLeftArrowBtn, false );
            }
            if ( forwardEnabled ) {
                enableArrow ( mRightArrowBtn, true );
            } else {
                enableArrow ( mRightArrowBtn, false );
            }
        }

        private void enableArrow ( ImageButton arrowBtn, boolean enabled ) {
            arrowBtn.setEnabled ( enabled );
            if ( enabled ) {
                arrowBtn.setAlpha ( ARROW_ALPHA_ENABLED );
            } else {
                arrowBtn.setAlpha ( ARROW_ALPHA_DISABLED );
            }
        }

        private void showArrow ( ImageButton arrowBtn, boolean show ) {
            if ( show ) {
                arrowBtn.setVisibility ( View.VISIBLE );
            } else {
                arrowBtn.setVisibility ( View.INVISIBLE );
            }
        }

        public boolean onTouch ( View v, MotionEvent event ) {
            if ( event.getAction() == MotionEvent.ACTION_DOWN ) {
                if ( v == mLeftArrowBtn ) {
                    mCvListener.onToRightGesture();
                } else if ( v == mRightArrowBtn ) {
                    mCvListener.onToLeftGesture();
                }
            } else if ( event.getAction() == MotionEvent.ACTION_UP ) {
                CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
                cv.enableActiveHighlight ( true );
            }
            return false;
        }

        // The reason why we handle candiate view's touch events here is because
        // that the view under the focused view may get touch events instead of the
        // focused one.
        @Override
        public boolean onTouchEvent ( MotionEvent event ) {
            event.offsetLocation ( -xOffsetForFlipper, 0 );
            CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
            cv.onTouchEventReal ( event );
            return true;
        }

        public void loadAnimation ( boolean animLeftRight, boolean forward ) {
            if ( animLeftRight ) {
                if ( forward ) {
                    if ( null == mInAnimPushLeft ) {
                        mInAnimPushLeft = createAnimation ( 1.0f, 0, 0, 0, 0, 1.0f,
                                                            ANIMATION_TIME );
                        mOutAnimPushLeft = createAnimation ( 0, -1.0f, 0, 0, 1.0f, 0,
                                                             ANIMATION_TIME );
                    }
                    mInAnimInUse = mInAnimPushLeft;
                    mOutAnimInUse = mOutAnimPushLeft;
                } else {
                    if ( null == mInAnimPushRight ) {
                        mInAnimPushRight = createAnimation ( -1.0f, 0, 0, 0, 0, 1.0f,
                                                             ANIMATION_TIME );
                        mOutAnimPushRight = createAnimation ( 0, 1.0f, 0, 0, 1.0f, 0,
                                                              ANIMATION_TIME );
                    }
                    mInAnimInUse = mInAnimPushRight;
                    mOutAnimInUse = mOutAnimPushRight;
                }
            } else {
                if ( forward ) {
                    if ( null == mInAnimPushUp ) {
                        mInAnimPushUp = createAnimation ( 0, 0, 1.0f, 0, 0, 1.0f,
                                                          ANIMATION_TIME );
                        mOutAnimPushUp = createAnimation ( 0, 0, 0, -1.0f, 1.0f, 0,
                                                           ANIMATION_TIME );
                    }
                    mInAnimInUse = mInAnimPushUp;
                    mOutAnimInUse = mOutAnimPushUp;
                } else {
                    if ( null == mInAnimPushDown ) {
                        mInAnimPushDown = createAnimation ( 0, 0, -1.0f, 0, 0, 1.0f,
                                                            ANIMATION_TIME );
                        mOutAnimPushDown = createAnimation ( 0, 0, 0, 1.0f, 1.0f, 0,
                                                             ANIMATION_TIME );
                    }
                    mInAnimInUse = mInAnimPushDown;
                    mOutAnimInUse = mOutAnimPushDown;
                }
            }
            mInAnimInUse.setAnimationListener ( this );
            mFlipper.setInAnimation ( mInAnimInUse );
            mFlipper.setOutAnimation ( mOutAnimInUse );
        }

        private Animation createAnimation ( float xFrom, float xTo, float yFrom,
                                            float yTo, float alphaFrom, float alphaTo, long duration ) {
            AnimationSet animSet = new AnimationSet ( getContext(), null );
            Animation trans = new TranslateAnimation ( Animation.RELATIVE_TO_SELF,
                    xFrom, Animation.RELATIVE_TO_SELF, xTo,
                    Animation.RELATIVE_TO_SELF, yFrom, Animation.RELATIVE_TO_SELF,
                    yTo );
            Animation alpha = new AlphaAnimation ( alphaFrom, alphaTo );
            animSet.addAnimation ( trans );
            animSet.addAnimation ( alpha );
            animSet.setDuration ( duration );
            return animSet;
        }

        private void startAnimation() {
            mFlipper.showNext();
        }

        private void stopAnimation() {
            mFlipper.stopFlipping();
        }

        public void onAnimationEnd ( Animation animation ) {
            if ( !mLeftArrowBtn.isPressed() && !mRightArrowBtn.isPressed() ) {
                CandidateView cv = ( CandidateView ) mFlipper.getCurrentView();
                cv.enableActiveHighlight ( true );
            }
        }

        public void onAnimationRepeat ( Animation animation ) {
        }

        public void onAnimationStart ( Animation animation ) {
        }
}
